import {Matrix} from "@/components/comm/matrix";
import {distanceRange, Position, range} from "@/components/comm/utils";

export type HexPosition = [number, number, number]
export type HexOpFunc<T, R> = (v: T, pos: HexPosition) => R;

export interface IHexContainer<T> extends Iterable<T> {
    get(pos: HexPosition, defaultValue?: T): T;

    set(pos: HexPosition, value: T): void;

    indexes(): Generator<HexPosition>;

    values(): Generator<T>;

    entries(): Generator<[HexPosition, T]>;
}

export abstract class HexContainer<T> implements IHexContainer<T> {
    protected data!: Matrix<T | undefined>;

    protected abstract cubeToIndex(pos: HexPosition): Position;

    protected abstract indexToCube(index: Position): HexPosition;

    public abstract contains(pos: HexPosition): boolean;

    public get(pos: HexPosition, defaultValue?: T): T {
        if (this.contains(pos)) {
            const index = this.cubeToIndex(pos);
            const res = this.data.get(index);
            if (res !== undefined) {
                return res;
            }
        }
        if (defaultValue !== undefined) {
            return defaultValue;
        } else {
            throw new Error(`下标[${pos.join(', ')}]越界！`);
        }
    }

    public set(pos: HexPosition, value: T): void {
        if (this.contains(pos)) {
            const index = this.cubeToIndex(pos);
            this.data.set(index, value);
        }
    }

    private* spiralIter6d(center: HexPosition, fromDis: number = 1, toDis: number = 1): Generator<HexPosition> {
        for (const d of distanceRange(fromDis, toDis)) {
            if (d === 0) {
                yield center;
                continue;
            }
            const [cx, cy, cz] = center;
            for (const x of range(d)) {
                yield [cx + x, cy + d - x, cz - d];
            }
            for (const y of range(0, -d, -1)) {
                yield [cx + d, cy + y, cz - d - y];
            }
            for (const z of range(d)) {
                yield [cx + d - z, cy - d, cz + z];
            }
            for (const x of range(0, -d, -1)) {
                yield [cx + x, cy - x - d, cz + d];
            }
            for (const y of range(d)) {
                yield [cx - d, cy + y, cz + d - y];
            }
            for (const z of range(0, -d, -1)) {
                yield [cx - d - z, cy + d, cz + z];
            }
        }
    }

    public* spiralIter(center: HexPosition, fromDis: number = 1, toDis: number = 1): Generator<HexPosition> {
        for (const pos of this.spiralIter6d(center, fromDis, toDis)) {
            if (this.contains(pos)) {
                yield pos;
            }
        }
    }

    public abstract indexes(): Generator<HexPosition>;

    public* values(): Generator<T> {
        for (const pos of this.indexes()) {
            yield this.get(pos);
        }
    }

    [Symbol.iterator](): Iterator<T> {
        return this.values();
    }

    public* entries(): Generator<[HexPosition, T]> {
        for (const pos of this.indexes()) {
            yield [pos, this.get(pos)];
        }
    }

    public static distance(pos1: HexPosition, pos2: HexPosition) {
        const [x1, y1, z1] = pos1;
        const [x2, y2, z2] = pos2;
        return Math.max(Math.abs(x1 - x2), Math.abs(y1 - y2), Math.abs(z1 - z2));
    }

    public forEvery(fn: HexOpFunc<T, void>): void {
        for (const pos of this.indexes()) {
            fn(this.get(pos), pos);
        }
    }

    public forNear(center: HexPosition, fn: HexOpFunc<T, void>,
                   fromDis: number = 1, toDis: number = 1): void {
        for (const pos of this.spiralIter(center, fromDis, toDis)) {
            fn(this.get(pos), pos);
        }
    }

    public swap(pos1: HexPosition, pos2: HexPosition): void {
        const obj1 = this.get(pos1);
        const obj2 = this.get(pos2);
        this.set(pos1, obj2);
        this.set(pos2, obj1);
    }

    public* rayIndexes(pos1: HexPosition, pos2: HexPosition,
                       fromDis: number = 1, toDis: number = 1): Generator<HexPosition> {
        const [x1, y1, z1] = pos1;
        const [x2, y2, z2] = pos2;
        const dx = Math.sign(x2 - x1);
        const dy = Math.sign(y2 - y1);
        const dz = Math.sign(z2 - z1);
        for (const d of distanceRange(fromDis, toDis)) {
            const rayPos: HexPosition = [x1 + dx * d, y1 + dy * d, z1 + dz * d];
            if (this.contains(rayPos)) {
                yield rayPos;
            }
        }
    }

    public forRay(pos1: HexPosition, pos2: HexPosition,
                  fn: HexOpFunc<T, void>, fromDis: number = 1, toDis: number = 1): void {
        for (const pos of this.rayIndexes(pos1, pos2, fromDis, toDis)) {
            fn(this.get(pos), pos);
        }
    }
}